#!/bin/bash
#/*
#**********************************************************************
#*
#* @filename  fan-ctrl
#*
#* @purpose   system daemon for controlling system fan pwm
#*
#* @create    2017/06/21
#*
#* @author    nixon.chu <nixon.chu@mic.com.tw>
#*
#* @history   2017/06/21: init version
#*
#**********************************************************************
#*/

DIR=$(dirname $0)

# include files
source ${DIR}/funcs.sh

#/*
#**********************************************************************
#*
#* CONSTANT VARIABLES
#*
#**********************************************************************
#*/

# process name/id
DAEMON_NAME=`basename $0`
DAEMON_PID="$$"

# define symbol for fan/temperature type sensor
SYMBOL_TEMP="TEMP_"
SYMBOL_FAN="FAN_"

# describe the structure of temperature sensor by id
STRUCT_TEMP_NAME=0
STRUCT_TEMP_CMD=1
STRUCT_TEMP_MAX=2
STRUCT_TEMP_SIZE=$(( ${STRUCT_TEMP_MAX} + 1 ))

# describe the structure of fan sensor by id
STRUCT_FAN_CMD=0
STRUCT_FAN_SIZE=$(( ${STRUCT_FAN_CMD} + 1 ))

# default fan zone configuration file
DEF_ZONE_CONF="${DIR}/fan-zone.conf"

# default fan zone thermal configuration file
DEF_TEMP_CONF="${DIR}/fan-zone-thermal.conf"

# default interval (sec)
DEF_INTERVAL=10

# default hysteresis (deg C)
DEF_HYSIS=3

# default debug mode (0: OFF, 1: ON)
DEF_DEBUG_MODE=0

# default log file
DEF_LOG_FILE="/var/log/syslog"

# default fan level
DEF_FAN_LEVEL=0

# default minimum pwm (38.25=255x15%)
DEF_MIN_PWM=39

# default zone id
DEF_ZONE_ID=0

# error codes
E_STATUS_GOOD=0
E_STATUS_FAULT=1
E_INVALID_ARGS=11
E_INVALID_CONF=12

# message levels
MSG_EMERG=0
MSG_ALERT=1
MSG_CRIT=2
MSG_ERR=3
MSG_WARNING=4
MSG_NOTICE=5
MSG_INFO=6
MSG_DEBUG=7

# default message level
DEF_MSG_LEVEL=$MSG_WARNING

#/*
#**********************************************************************
#*
#* GLOBAL VARIABLES
#*
#**********************************************************************
#*/

# temperature sensor group
GBL_TEMP_GRP=()
GBL_TEMP_NUM=${#GBL_TEMP_GRP[@]}

# fan sensor group
GBL_FAN_GRP=()
GBL_FAN_NUM=${#GBL_FAN_GRP[@]}

# fan level table
GBL_FAN_LEVEL=()
GBL_LEVEL_NUM=${#GBL_FAN_LEVEL[@]}

# load defaults
GBL_ZONE_CONF=$DEF_ZONE_CONF
GBL_TEMP_CONF=$DEF_TEMP_CONF
GBL_INTERVAL=$DEF_INTERVAL
GBL_HYSIS=$DEF_HYSIS
GBL_DEBUG_MODE=$DEF_DEBUG_MODE
GBL_LOG_FILE=$DEF_LOG_FILE
GBL_CUR_LEVEL=$DEF_FAN_LEVEL
GBL_MIN_PWM=$DEF_MIN_PWM
GBL_ZONE_ID=$DEF_ZONE_ID
GBL_MSG_LEVEL=$DEF_MSG_LEVEL

# temperature sensors' readings/statuses/properties (properties list have to be converted with fan level table)
GBL_TEMP_READINGS=()
GBL_TEMP_STATUSES=()
GBL_TEMP_PROPERTY=()

#/*
#**********************************************************************
#*
#* FUNCTIONS
#*
#**********************************************************************
#*/

#/*
#* FEATURE:
#*   usage
#* PURPOSE:
#*   show the usage of this daemon
#* PARAMETERS:
#*
#* RETURNS:
#*   success, this function returns @E_INVALID_ARGS.
#*/
function usage() {
  local dbg_mode

  [ $GBL_DEBUG_MODE -eq 0 ] && dbg_mode="off" || dbg_mode="on"

  echo -e "Usage:  $0 [-z zone_file] [-t thermal_file] [-o log_file] [-d]" >&2
  echo -e "" >&2
  echo -e "Arguments:" >&2
  echo -e "       -z, --zone-config    Fan zone configuration file (default: $DEF_ZONE_CONF)" >&2
  echo -e "       -t, --temp-config    Fan zone thermal configuration file (default: $DEF_TEMP_CONF)" >&2
  echo -e "       -o, --log-file       Log file (default: $DEF_LOG_FILE)" >&2
  echo -e "       -d, --debug          Debug mode (default: $dbg_mode)" >&2
  exit ${E_INVALID_ARGS}
}

#/*
#* FEATURE:
#*   print_msg
#* PURPOSE:
#*   print message by message level
#* PARAMETERS:
#*   msg_lvl              (IN) message level
#*   msg                  (IN) message
#* RETURNS:
#*
#*/
function print_msg() {
  local msg_lvl=$1
  local msg=$2

  [ $msg_lvl -le $GBL_MSG_LEVEL ] && echo "${msg}" >&2
}

#/*
#* FEATURE:
#*   err_msg
#* PURPOSE:
#*   log error message
#* PARAMETERS:
#*   msg                  (IN) message
#*   err_no               (IN) error code
#* RETURNS:
#*   success, this function returns non-zero error code.
#*/
function err_msg() {
  local msg=$1
  local err_no=$2

  log_msg $MSG_ERROR "${msg}"
  exit ${err_no}
}

#/*
#* FEATURE:
#*   log_msg
#* PURPOSE:
#*   log message
#* PARAMETERS:
#*   msg_lvl              (IN) message level
#*   msg                  (IN) message
#* RETURNS:
#*
#*/
function log_msg() {
  local msg_lvl=$1
  local msg=$2

  if [ $GBL_LOG_FILE == $DEF_LOG_FILE ]; then
    `logger -t $DAEMON_NAME -p $msg_lvl $msg`
  else
    echo -e "`date +"%b %_d %T"` `hostname` $DAEMON_NAME[$DAEMON_PID]: ${msg}" >> ${GBL_LOG_FILE}
  fi

  print_msg $msg_lvl "${msg}"
}

#/*
#* FEATURE:
#*   debug_temp_grp
#* PURPOSE:
#*   debug function for showing the temperature sensor group configs
#*   @GBL_TEMP_GRP[]: store sensor group configs (sensor name, command, and max_temp)
#* PARAMETERS:
#*
#* RETURNS:
#*
#*/
function debug_temp_grp() {
  if [ $GBL_DEBUG_MODE -ne 0 ]; then
    echo "********* Sensor Group *********"
    for ((i=0; i<$GBL_TEMP_NUM; i++))
    do
      echo -en "`fetch_temp_struct $i $STRUCT_TEMP_NAME`\t"
      echo -en "`fetch_temp_struct $i $STRUCT_TEMP_CMD`\t"
      echo "`fetch_temp_struct $i $STRUCT_TEMP_MAX`"
    done
  fi
}

#/*
#* FEATURE:
#*   debug_temp_readings
#* PURPOSE:
#*   debug function for showing the temperature sensors' readings
#*   @GBL_TEMP_READINGS[]: store current reading for temperature sensors
#* PARAMETERS:
#*
#* RETURNS:
#*
#*/
function debug_temp_readings() {
  if [ $GBL_DEBUG_MODE -ne 0 ]; then
    echo "********* Sensor Reading *********"
    for ((i=0; i<$GBL_TEMP_NUM; i++))
    do
      echo -e "`fetch_temp_struct $i $STRUCT_TEMP_NAME`\t${GBL_TEMP_READINGS[$i]}"
    done
  fi
}

#/*
#* FEATURE:
#*   debug_temp_property
#* PURPOSE:
#*   debug function for showing the temperature sensors' properties
#*   @GBL_TEMP_PROPERTY[]: store temperature sensors' properties (asserted temperature for each fan levels)
#* PARAMETERS:
#*
#* RETURNS:
#*
#*/
function debug_temp_property() {
  local size base start_idx level
  if [ $GBL_DEBUG_MODE -ne 0 ]; then
    echo "********* Sensor Table *********"
    for ((i=0; i<${GBL_TEMP_NUM}; i++))
    do
      # show array @GBL_TEMP_PROPERTY[]: format name(1) + fan levels(2-?)
      size=$(( ${GBL_LEVEL_NUM}+1 ))
      base=$i*${size}
      start_idx=$(( $base+1 ))
      level=${GBL_LEVEL_NUM}
      echo -e "${GBL_TEMP_PROPERTY[$base]}\t\t${GBL_TEMP_PROPERTY[@]:$start_idx:$level}"
    done
  fi
}

#/*
#* FEATURE:
#*   fetch_temp_struct
#* PURPOSE:
#*   fetch the member value of specific temperature sensor
#* PARAMETERS:
#*   id                   (IN) sensor id
#*   member_id            (IN) member id
#* RETURNS:
#*   success, returns member value
#*/
function fetch_temp_struct() {
  local id member_id index_base data

  id=$1
  member_id=$2

  index_base=$(( $id * $STRUCT_TEMP_SIZE ))
  data=${GBL_TEMP_GRP[$(($index_base+$member_id))]}
  # remove unuseful characters, such as double quotes('"') and the start/end space(' ') of a string.
  data=`echo $data | sed 's/^ *\| *$//g' | sed 's/^"\|\"$//g'`
  echo $data
}

#/*
#* FEATURE:
#*   fetch_temp_property
#* PURPOSE:
#*   fetch the properties of specific temperature sensor
#* PARAMETERS:
#*   name                 (IN) sensor name
#* RETURNS:
#*   success, returns an asserted temperature list
#*/
function fetch_temp_property() {
  local name size base start_idx length

  name=$1

  size=$(( $GBL_LEVEL_NUM+1 ))
  for ((i=0; i<${GBL_TEMP_NUM}; i++))
  do
    base=$i*${size}
    # find sensor by name
    if [ ${GBL_TEMP_PROPERTY[${base}]} == "${name}" ]; then
      start_idx=$(( $base+1 ))
      length=$GBL_LEVEL_NUM
      echo ${GBL_TEMP_PROPERTY[@]:$start_idx:$length}
      break
    fi
  done
}

#/*
#* FEATURE:
#*   debug_fan_speed
#* PURPOSE:
#*   debug function for showing system current fan level and PWM
#*   @GBL_CUR_LEVEL: system current/output fan level
#* PARAMETERS:
#*
#* RETURNS:
#*
#*/
function debug_fan_speed() {
  if [ $GBL_DEBUG_MODE -ne 0 ]; then
    # For human readable, the representation of fan level will start from 1.
    echo "Current Level: $(( $GBL_CUR_LEVEL+1 )), PWM: ${GBL_FAN_LEVEL[$GBL_CUR_LEVEL]}"
  fi
}

#/*
#* FEATURE:
#*   arg_parse
#* PURPOSE:
#*   parser for input arguments
#* PARAMETERS:
#*   arg_lists[]          (IN) argument list
#* RETURNS:
#*
#*/
function arg_parse() {
  while [[ $# -ge 1 ]]
  do
    key="$1"
    case $key in
      -z|--zone-config)
        [ $# -lt 2 ] && usage
        GBL_ZONE_CONF=$2
        [ ! -e $GBL_ZONE_CONF ] && usage
        shift # past argument
      ;;
      -t|--temp-config)
        [ $# -lt 2 ] && usage
        GBL_TEMP_CONF=$2
        [ ! -e $GBL_TEMP_CONF ] && usage
        shift # past argument
      ;;
      -o|--log-file)
        [ $# -lt 2 ] && usage
        GBL_LOG_FILE=$2
        shift # past argument
      ;;
      -d|--debug)
        GBL_DEBUG_MODE=1
      ;;
      *)
        usage # unknown option
      ;;
    esac
    shift # past argument or value
  done
}

#/*
#* FEATURE:
#*   valid_conf
#* PURPOSE:
#*   check if configuration files are valid
#* PARAMETERS:
#*
#* RETURNS:
#*   success, this function returns nothing
#*   fail, returns @E_INVALID_CONF
#*/
function valid_conf() {
  local list_1 list_2 match_num

  list_1=("${!1}")
  list_2=("${!2}")

  # validate zone config
  [ ${GBL_TEMP_NUM} -eq 0 ] && err_msg "config: status=invalid. reason=no TEMPERATURE SENSORS defined." "${E_INVALID_CONF}"
  [ ${GBL_FAN_NUM} -eq 0 ]  && err_msg "config: status=invalid. reason=no FAN defined."                 "${E_INVALID_CONF}"

  # validate thermal config
  [ ${GBL_LEVEL_NUM} -eq 0 ] && err_msg "config: status=invalid. reason=no FAN LEVELS defined."         "${E_INVALID_CONF}"
  [ ${#list_2[@]} -ne ${GBL_TEMP_NUM} ]  && err_msg "config: status=invalid. reason=the number of temperature sensors is inconsistent." "${E_INVALID_CONF}"

  # compare temperature sensor name between configuration files
  match_num=0
  for i in ${!list_1[@]}
  do
    for j in ${!list_2[@]}
    do
      [ "${list_1[$i]}" == "${list_2[$j]}" ] && match_num=$(( $match_num+1 ))
    done
  done
  [ $match_num -ne ${GBL_TEMP_NUM} ] && err_msg "config: status=invalid. reason=the name of temperature sensors is inconsistent." "${E_INVALID_CONF}"

  # validate pwm values
  for pwm in ${GBL_FAN_LEVEL[@]}
  do
    expr $pwm + $GBL_MIN_PWM >/dev/null 2>&1
    [ $? -ne 0 ] && err_msg "config: status=invalid. reason=the value of pwm is not integer." "$E_INVALID_CONF"
    [ $pwm -lt $GBL_MIN_PWM ] && err_msg "config: status=invalid. reason=the value of fan level is less than MIN_PWM($GBL_MIN_PWM)." "$E_INVALID_CONF"
  done
}

#/*
#* FEATURE:
#*   initialize
#* PURPOSE:
#*   load zone config and thermal config
#* PARAMETERS:
#*
#* RETURNS:
#*
#*/
function initialize() {
  local data conf_interval conf_hysis conf_min_pwm conf_zone_id conf_msg_level temp_list_1 temp_list_2

  log_msg $MSG_INFO "Initializing fan control service..."

  # fetch temperature sensor name from configuration files
  temp_list_1=(`cat ${GBL_ZONE_CONF} | grep "^${SYMBOL_TEMP}" | awk -F "," {'print $1'}`)
  temp_list_2=(`cat ${GBL_TEMP_CONF} | grep "^${SYMBOL_TEMP}" | awk {'print $1'}`)

  # calculate the number of temperature/fan sensors
  GBL_TEMP_NUM=${#temp_list_1[@]}
  GBL_FAN_NUM=(`cat ${GBL_ZONE_CONF} | grep "^${SYMBOL_FAN}" | awk {'print $2'} | wc -l`)

  # calculate the number of fan levels
  GBL_FAN_LEVEL=(`cat ${GBL_TEMP_CONF} | grep "^PWM" | awk -F "PWM" {'print $2'}`)
  GBL_LEVEL_NUM=${#GBL_FAN_LEVEL[@]}

  # check if the interval/hyteresis should be updated.
  conf_interval=`cat ${GBL_ZONE_CONF} | grep "^INTERVAL=" | awk -F "=" {'print $2'} | tail`
  [ ! -z ${conf_interval} ] && GBL_INTERVAL=${conf_interval}
  conf_hysis=`cat ${GBL_ZONE_CONF} | grep "^HYTERESIS=" | awk -F "=" {'print $2'} | tail`
  [ ! -z ${conf_hysis} ] && GBL_HYSIS=${conf_hysis}
  conf_min_pwm=`cat ${GBL_ZONE_CONF} | grep "^MIN_PWM=" | awk -F "=" {'print $2'} | tail`
  [ ! -z ${conf_min_pwm} ] && GBL_MIN_PWM=${conf_min_pwm}
  conf_zone_id=`cat ${GBL_ZONE_CONF} | grep "^ZONE_ID=" | awk -F "=" {'print $2'} | tail`
  [ ! -z ${conf_zone_id} ] && GBL_ZONE_ID=${conf_zone_id}
  conf_msg_level=`cat ${GBL_ZONE_CONF} | grep "^MSG_LEVEL=" | awk -F "=" {'print $2'} | tail`
  [ ! -z ${conf_msg_level} ] && GBL_MSG_LEVEL=${conf_msg_level}

  # load zone config
  for ((i=1; i<=${GBL_TEMP_NUM}; i++))
  do
    GBL_TEMP_STATUSES+=($E_STATUS_GOOD)
    data=`cat ${GBL_ZONE_CONF} | grep "^${SYMBOL_TEMP}" | awk NR==$i`
    # FIXME
    GBL_TEMP_GRP+=("`echo $data | awk -F ", " {'print $1'}`")
    GBL_TEMP_GRP+=("`echo $data | awk -F ", " {'print $2'}`")
    GBL_TEMP_GRP+=("`echo $data | awk -F ", " {'print $3'}`")
  done
  debug_temp_grp

  for ((i=1; i<=${GBL_FAN_NUM}; i++))
  do
    data=`cat ${GBL_ZONE_CONF} | grep "^${SYMBOL_FAN}" | awk NR==$i{'print $2'}`
    GBL_FAN_GRP+=("`echo $data`")
  done

  # load thermal config
  for ((i=0; i<${GBL_TEMP_NUM}; i++))
  do
    index=$(( $i * ${STRUCT_TEMP_SIZE} + ${STRUCT_TEMP_NAME} ))
    sensor_name=${GBL_TEMP_GRP[$index]}
    GBL_TEMP_PROPERTY+=(`cat ${GBL_TEMP_CONF} | grep "^${sensor_name}" | tail`)
  done
  debug_temp_property

  # check if the config files is valid
  valid_conf temp_list_1[@] temp_list_2[@]
}

#/*
#* FEATURE:
#*   temperature_collection
#* PURPOSE:
#*   collect all temperature sensor readlings
#* PARAMETERS:
#*
#* RETURNS:
#*   success, returns a series of reading @GBL_TEMP_READINGS[]
#*/
function temperature_collection() {
  local name cmd max_temp reading status pre_status

  GBL_TEMP_READINGS=()
  for ((i=0; i<${GBL_TEMP_NUM}; i++))
  do
    # read temperature sensor value & record sensor status
    name=`fetch_temp_struct $i ${STRUCT_TEMP_NAME}`
    cmd=`fetch_temp_struct $i ${STRUCT_TEMP_CMD}`
    max_temp=`fetch_temp_struct $i ${STRUCT_TEMP_MAX}`
    reading=`eval ${cmd} 2>/dev/null`
    if [ $? -eq 0 ]; then
      GBL_TEMP_READINGS+=($reading)
      status=$E_STATUS_GOOD
    else
      GBL_TEMP_READINGS+=($max_temp)
      status=$E_STATUS_FAULT
    fi

    # Compare previous status of temperature sensor, then update it.
    pre_status=${GBL_TEMP_STATUSES[$i]}
    if [ $pre_status -eq $E_STATUS_GOOD ] && [ $status -eq $E_STATUS_FAULT ]; then
      # Good -> Fault
      log_msg $MSG_WARN "[ZONE_${GBL_ZONE_ID}] Sensor $name: status=ERROR. reason=read sensor failed."
      elif [ $pre_status -eq $E_STATUS_FAULT ] && [ $status -eq $E_STATUS_GOOD ]; then
      # Fault -> Good
      log_msg $MSG_WARN "[ZONE_${GBL_ZONE_ID}] Sensor $name: status=RECOVER. reason=reading is $reading deg C"
    fi
    GBL_TEMP_STATUSES[$i]=$status
  done
  debug_temp_readings
}

#/*
#* FEATURE:
#*   fan_level_selection
#* PURPOSE:
#*   select proper fan level by temperature sensor reading
#* PARAMETERS:
#*   reading_list         (IN) sensor reading list
#* RETURNS:
#*   success, returns a fan level value
#*/
function fan_level_selection() {
  local reading_list name reading temp_list highest_level pre_level next_level hysis_temp res res1

  reading_list=("${!1}")
  highest_level=0
  pre_level=${GBL_CUR_LEVEL}
  next_level=0

  # search fan table by each temperature sensor reading
  for ((i=0; i<${GBL_TEMP_NUM}; i++))
  do
    name=`fetch_temp_struct $i $STRUCT_TEMP_NAME`
    reading=${reading_list[$i]}
    temp_list=(`fetch_temp_property ${name}`)
    for j in ${!temp_list[@]}
    do
      res=`echo $reading \<= ${temp_list[$j]} | bc -l`
      if [ $res -eq 1 ] || [ $j -eq $((${#temp_list[@]}-1)) ]; then
        if [ $j -gt $highest_level ]; then
          highest_level=$j
        fi
        break
      fi
    done
  done

  if [ $highest_level -lt $pre_level ]; then
    # Determine if require decreasing current fan level @assert_level
    local pass_num
    pass_num=0
    for ((i=0; i<${GBL_TEMP_NUM}; i++))
    do
      name=`fetch_temp_struct $i $STRUCT_TEMP_NAME`
      reading=${reading_list[$i]}
      temp_list=(`fetch_temp_property ${name}`)
      assert_level=$(( $pre_level-1 ))
      hysis_temp=`echo ${temp_list[$assert_level]} - $GBL_HYSIS | bc`
      res=`echo $reading \<= $hysis_temp | bc -l`
      if [ $res -eq 1 ]; then
        pass_num=$(( $pass_num+1 ))
      else
        break
      fi
    done
    if [ $pass_num -eq ${GBL_TEMP_NUM} ]; then
      # Decrease fan pwm: All sensor reading should less than its own hysteresis temperature
      next_level=${highest_level}
    else
      # Keep current fan pwm
      next_level=${pre_level}
    fi
  else
    # Increase fan pwm
    next_level=${highest_level}
  fi
  GBL_CUR_LEVEL=${next_level}
}

#/*
#* FEATURE:
#*   apply_fan_level
#* PURPOSE:
#*   convert given fan level to PWM, then output to fans
#* PARAMETERS:
#*   fan_list             (IN) fan devices list
#*   fan_level            (IN) fan level
#* RETURNS:
#*   success, returns a pwm value
#*/
function apply_fan_level() {
  local fan_level fan_list

  fan_list=("${!1}")
  fan_level=$2

  for fan in ${fan_list[@]}
  do
    echo ${GBL_FAN_LEVEL[$fan_level]} > $fan
  done
  debug_fan_speed
}

#/*
#**********************************************************************
#*
#* MAIN
#*
#**********************************************************************
#*/
Platform_init
arg_parse $@
initialize
log_msg $MSG_INFO "Starting fan control service..."
while [ true ]; do
  temperature_collection
  fan_level_selection GBL_TEMP_READINGS[@]
  apply_fan_level GBL_FAN_GRP[@] ${GBL_CUR_LEVEL}
  sleep $GBL_INTERVAL
done

exit 0
